Skip to main content

第 11 章:設定伺服器環境與程式

佈建伺服器方式

  • 使用客製化 AMI
    • 使用 packer 服務,或是在雲端服務直接建立客製化 AMI
  • 使用標準 AMI 後透過指令安裝相關程式
    • file upload
    • exec
    • Chef, Puppet, Ansible
    • user data in cloud service

簡單的網頁伺服器

一台簡單的網頁伺服器需要的工作:

  1. 建立一個虛擬網路
  2. 切出一個網段
  3. 開啟一個虛擬機
  4. 放到 SSH 公鑰
  5. 設定防火牆
  6. 安裝 nginx 做為網頁服務引擎

要安裝 nginx,的方法有很多,可以登入伺服器,下指令安裝。但是這樣不符合,IaC 的精神。

我們來試試 Terraform 的佈建器 (provisioner)

佈建器 provisioner

用佈建器來完成在伺服器上安裝 nginx 這件工作。我們會用到兩個區塊,connction 跟 provisioner 區塊,兩個都要放在 aws_instance 裡面。

首先是 connection 區塊,常用的引數 (Arguments) 有:

  • type: 支援 ssh 跟 winrm
  • user: 建立連線所使用的使用者
  • host: 伺服器的 IP
  • private_key: ssh 私鑰

再來是 provisioner 區塊,有分 filelocal-exec 跟 remote-exec 三種。

provisioner example
resource "aws_key_pair" "edward-key"{
ami = "$(lookup(var.AMIS,var.AWS_REGION))"
instance_type = "t2.micro"
key_name = "$(aws_keypair.mykey.key_name)"

provisioner "file"{
source = "script.sh"
destination = "/opt/script.sh"
connection{
user = "${var.instance_username}"
private_key = "${file({var.path_to_private_key})}"
}
}
provisioner "remote-exec"{
inline = [
"chmod +x /opt/script.sh",
"/opt/script.sh arguments"
]
}
}

我們要在伺服器執行一些指令,使用的是 remote-exec,主要的引數:

  • inline: 指令清單

完整範例

把輸入變數 (Input Variables) 跟輸出值 (Output Values) 加入組態檔,並使用佈建器 (provisioner) 來安裝服務。

完成的檔案清單:

  • variables.tf
  • main.tf
  • outputs.tf
  • terraform.tfvars
variables.tf
variable "aws_region" {
type = string
description = "AWS region to launch servers."
default = "ap-northeast-1"
}

variable "cidr" {
type = string
description = "vpc cidr block"
}

variable "public_subnet" {
type = string
description = "public subnet cidr block"
}

variable "public_key_path" {
type = string
description = "Path to SSH public key"
default = "~/.ssh/id_rsa.pub"
}

variable "private_key_path" {
type = string
description = "Path to SSH private key"
default = "~/.ssh/id_rsa"
}

variable "ami" {
type = string
description = "ami id"
}

variable "my_ip" {
type = string
description = "my ip to allow ssh connection"
}

main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "3.5.0"
}
}
}

provider "aws" {
profile = "default"
region = "ap-northeast-1"
}

resource "aws_vpc" "this" {
cidr_block = var.cidr
}

resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
}

resource "aws_route" "internet_access" {
route_table_id = aws_vpc.this.main_route_table_id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}

resource "aws_subnet" "this" {
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnet
map_public_ip_on_launch = true
}

resource "aws_security_group" "ssh" {
name = "ssh"
description = "sg for ssh incoming"
vpc_id = aws_vpc.this.id

ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [format("%s/32", var.my_ip)]
}
}

resource "aws_security_group" "web" {
name = "web"
description = "sg for web incoming"
vpc_id = aws_vpc.this.id

ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

# outbound internet access
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

resource "aws_key_pair" "practice" {
key_name = "practice"
public_key = file(var.public_key_path)
}

resource "aws_instance" "web" {
ami = var.ami
instance_type = "t2.micro"

key_name = aws_key_pair.practice.id

vpc_security_group_ids = [
aws_security_group.ssh.id,
aws_security_group.web.id,
]

subnet_id = aws_subnet.this.id

connection {
type = "ssh"
user = "ubuntu"
host = self.public_ip
private_key = file(var.private_key_path)
}

provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx",
]
}
}

output.tf
output "vpc_id" {
description = "ID of VPC"
value = aws_vpc.this.id
}

output "web_instance_id" {
description = "ID of web instance"
value = aws_instance.web.id
}

output "web_public_ip" {
description = "Public IP of web server"
value = aws_instance.web.public_ip
}

terraform.tfvars
cidr            = "10.0.0.0/16"
public_subnet = "10.0.1.0/24"
public_key_path = "~/.ssh/id_rsa.pub"

# Ubuntu Server 20.04 LTS (HVM), SSD Volume Type, 64-bit x86
ami = "ami-0461b11e2fad8c14a"

執行

先查詢你目前的 public ip,接著執行 apply 指令並帶入變數

terraform apply -var="my_ip=xxx.xxx.xxx.xxx"
...

Apply complete! Resources: 8 added, 0 changed, 0 destroyed.

Outputs:

vpc_id = vpc-03fa104e06a386d5d
web_instance_id = i-02726dce6cb801181
web_public_ip = 123.123.123.123

完成了就可以開啟 http://123.123.123.123 驗收一下網頁服務。

佈建器 (provisioner) 這個方法並不是 Terraform 推薦的方法,在測試的過程中我有遇到幾次的失敗。

後面我們再找其他的辦法來試試

前面我們所使用佈建器 (Provisioner) 的 remote-exec 不怎麼好用,而且 Terraform 無法檢查設定有沒有變動。

aws 有一個叫做使用者資料 (User Data) 的功能,可以輔助我們設定虛擬機。只要在建立機器時傳送使用者資料 (User Data),在執行個體 (instance) 啟動之後就會執行指令。

使用者資料 (User Data)

使用者資料是基於 cloud-init 這個工具在運作的。cloud-init 是由 Canonical 所發佈的工具

目的就是要讓雲端虛擬機初始化可以更容易的自動化。目前常見的雲端平台跟多數的 Linux 作業系統都有支援 cloud-init 功能。

使用者資料可以使用 gzip 壓縮,大小通常會有 16384 位元組的上限。支援兩種格式:

  • shell 腳本: 檔案以 #! 開始
  • cloud-int 組態: 檔案以 #cloud-config 開始

以下提供幾個簡單的範例,完整的設定方式請詳閱 Modules - cloud-init

建立使用者

使用 users 建立使用者,並可以設定 sudo 權限,放入公鑰等等。

#cloud-configusers:
- default
- name: barfoo
sudo: ALL=(ALL) NOPASSWD:ALL
groups: users, admin
ssh_import_id: None
lock_passwd: true
ssh_authorized_keys:
- <ssh pub key 1>

建立檔案

使用 write_files 把檔案放到目的地。

#cloud-configwrite_files:
- path: /var/www/html/terraform.html
content: |
<h1>Provisioning via Terraform</h1>

安裝套件

要更新 apt 套件資料庫的話,要把 package_update 設定為 true

要安裝的套件全部列在 packages 下面。

#cloud-configpackage_update: true
packages:
- nginx
- git

Reference

我們來實際用使用者資料設定虛擬機,以下是預計要讓使用者資料處理的工作:

  • 建立一個叫做 terraform 的使用者
  • 安裝 nginx
  • 放入一個 HTML 檔案

建立使用者資料

建立使用者資料的檔案 user_data.yaml

user_data.yaml
#cloud-config# create usersusers:
- default
- name: terraform
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash

# install nginxpackage_update: true
packages:
- nginx

# put html filewrite_files:
- path: /var/www/html/terraform.html
content: |
<h1>Provisioning via Terraform</h1>

設定組態檔

使用 template_file 這個 data 區塊來載入 user_data.yaml,再把載入的資料放到 aws_instance 的 user_data 引數中。

data template_file 的引數:

  • template: 放模版資料,可以用 file() 函數來載入檔案。

上面的設定都放在 main.tf 裡面,完整檔案可以參考: https://github.com/nyogjtrc/practice-terraform/tree/master/web-server-user-data

main.tf
data "template_file" "user_data" {
template = file("user_data.yaml")
}

resource "aws_instance" "web" {
ami = var.ami
instance_type = "t2.micro"

key_name = aws_key_pair.practice.id

vpc_security_group_ids = [
aws_security_group.ssh.id,
aws_security_group.web.id,
]

subnet_id = aws_subnet.this.id

user_data = data.template_file.user_data.rendered

tags = {
Name = "Web-Terraform"
topic = "web-server-user-data"
}
}

執行 terraform

terraform apply
...
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.

Outputs:

vpc_id = vpc-0db96967262b14fcf
web_instance_id = i-008d3a7371fba33b2
web_public_ip = 18.177.119.161

使用者資料是在虛擬機建立之後才執行的,所以需要稍微等一下

aws 自動分配了 18.177.119.161 給虛擬機,在瀏覽器開啟 http://18.177.119.161/terraform.html 就可以看到我們剛剛放上去的檔案